AWS CDKでCognito認証されたAPI Gateway(HTTP API)を構築する
はじめに
おはようございます、加藤です。今回はAWS CDKを使ってAmazon API Gateway(HTTP API = v2)にAmazon Cognitoを使って認証を設定する方法をまとめてみます。 また作成されたAPIに対してOpenAPI定義を作成し、それをSwagger UIでプレビュー&(認証された状態で)API呼び出しする方法も合わせて説明します。
リポジトリのセットアップ
aws-cdkコマンドを使ってリポジトリを生成します。
mkdir cdk-demo-apigw-with-cognito cd cdk-demo-apigw-with-cognito npx -p aws-cdk cdk init app --language typescript
必要な依存関係をインストールします。
npm i -D \ @aws-cdk/aws-apigatewayv2 \ @aws-cdk/aws-apigatewayv2-integrations \ @aws-cdk/aws-apigatewayv2-authorizers \ @aws-cdk/aws-cognito \ @aws-cdk/aws-lambda-nodejs \ @types/aws-lambda \ esbuild@0
AWSリソースの構築
API GatewayでCognitoを使って認証するためにはCognito UserPoolとUserPool Clientを作成し、それをAuthorizerとしてAPIに関連付けします。
気をつけて欲しいポイントとしてパス/
へのアクセスは特別なものとして扱われ、特にCORS周りで特殊な設定が必要になるので特別な理由が無ければ使わないほうが無難です。
HTTP API のルートの使用 - Amazon API Gateway # $default ルートの操作
// lib/cdk-demo-apigw-with-cognito-stack.ts import * as cdk from '@aws-cdk/core'; import * as cognito from '@aws-cdk/aws-cognito'; import * as apigw from '@aws-cdk/aws-apigatewayv2'; import {HttpMethod} from '@aws-cdk/aws-apigatewayv2/lib/http/route'; import * as intg from '@aws-cdk/aws-apigatewayv2-integrations'; import * as nodejs from '@aws-cdk/aws-lambda-nodejs'; import * as authz from '@aws-cdk/aws-apigatewayv2-authorizers'; export interface CdkDemoApigwWithCognitoStackProps extends cdk.StackProps { callbackUrls: string[]; logoutUrls: string[]; frontendUrls: string[]; domainPrefix: string; } export class CdkDemoApigwWithCognitoStack extends cdk.Stack { constructor( scope: cdk.Construct, id: string, props: CdkDemoApigwWithCognitoStackProps ) { super(scope, id, props); const userPool = new cognito.UserPool(this, 'userPool', { selfSignUpEnabled: false, standardAttributes: { email: {required: true, mutable: true}, phoneNumber: {required: false}, }, signInCaseSensitive: false, autoVerify: {email: true}, signInAliases: {email: true}, accountRecovery: cognito.AccountRecovery.EMAIL_ONLY, removalPolicy: cdk.RemovalPolicy.DESTROY, }); userPool.addDomain('domain', { cognitoDomain: {domainPrefix: props.domainPrefix}, }); const userPoolClient = userPool.addClient('client', { oAuth: { scopes: [ cognito.OAuthScope.EMAIL, cognito.OAuthScope.OPENID, cognito.OAuthScope.PROFILE, ], callbackUrls: props.callbackUrls, logoutUrls: props.logoutUrls, flows: {authorizationCodeGrant: true}, }, }); const handler = new nodejs.NodejsFunction(this, 'Handler'); const authorizer = new authz.HttpUserPoolAuthorizer({ authorizerName: 'CognitoAuthorizer', userPool, userPoolClient, }); const httpApi = new apigw.HttpApi(this, 'Api', { defaultAuthorizer: authorizer, corsPreflight: { allowOrigins: props.frontendUrls, allowMethods: [apigw.CorsHttpMethod.ANY], allowHeaders: ['authorization'], }, }); httpApi.addRoutes({ methods: [HttpMethod.GET], path: '/pets', integration: new intg.LambdaProxyIntegration({handler}), }); new cdk.CfnOutput(this, 'OutputApiUrl', {value: httpApi.url!}); new cdk.CfnOutput(this, 'OutputDomainPrefix', {value: props.domainPrefix}); } }
API Gatewayのバックエンドで処理を行うLambda関数を作成します。@aws-cdk/aws-lambda-nodejs
はプロパティを指定しないと${stackFileName}.${CdkComponentId}.ts
のファイルをエントリーポイントとしてesbuildを使ってバンドリングを行います。(コンポーネントIDは大文字小文字を区別します)
下記のようにペット一覧のモックデータを返す処理を実装します。
// lib/cdk-demo-apigw-with-cognito-stack.Handler.ts import {APIGatewayProxyHandlerV2} from 'aws-lambda'; interface Pet { name: string; age: number; } interface Response { pets: Pet[]; } export const handler: APIGatewayProxyHandlerV2 = async () => { return { statusCode: 200, body: JSON.stringify({ pets: [ { name: 'hina', age: 1, }, { name: 'koharu', age: 2, }, { name: 'konatsu', age: 3, }, ], } as Response), }; };
エントリーファイルを変更し、スタックが求めるプロパティを注入します。
// bin/cdk-demo-apigw-with-cognito.ts #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import {CdkDemoApigwWithCognitoStack} from '../lib/cdk-demo-apigw-with-cognito-stack'; const app = new cdk.App(); new CdkDemoApigwWithCognitoStack(app, 'CdkDemoApigwWithCognitoStack', { callbackUrls: ['http://localhost:3200/'], logoutUrls: ['http://localhost:3200/'], frontendUrls: ['http://localhost:3200'], domainPrefix: process.env.DOMAIN_PREFIX!, });
環境変数DOMAIN_PREFIX
を設定し、デプロイします。
デプロイ時に出力されたOutputは後ほどOpenAPI定義を作成する際とSwagger UIを使う際に使用するのでメモしておいてください。
DOMAIN_PREFIX=任意の英数記号(一部の予約語は使用できない、cognitoなど) npm run cdk deploy
Swagger UIでのプレビューとAPI呼び出し
OpenAPI定義を作成します。なお、プレースホルダーの部分をメモしたOutputに差し替えてください。
# docs/openapi.yaml openapi: 3.0.3 info: title: Petstore API overview version: 1.0.0 servers: - url: {{OutputApiUrl}} components: securitySchemes: OAuth2: type: oauth2 description: For more information, see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html flows: authorizationCode: authorizationUrl: https://{{OutputDomainPrefix}}.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize tokenUrl: https://{{OutputDomainPrefix}}.auth.ap-northeast-1.amazoncognito.com/oauth2/token scopes: openid: openid token paths: /pets: get: operationId: listPets security: - OAuth2: [ openid, token ] responses: 200: description: 200 OK content: application/json: schema: required: - pets properties: pets: type: array items: required: - name - age properties: name: type: string age: type: number example: pets: - name: hina age: 1 - name: koharu age: 2 - name: konatsu age: 3
このOpenAPI定義をSwagger UIでプレビューします。Docker上で実行する為にdocker-compose.yaml
を書きます。
# docker-compose.yaml version: '3.8' services: swagger: image: swaggerapi/swagger-ui environment: API_URL: /swagger.yaml BASE_URL: / env_file: .env volumes: - ./docs/openapi.yaml:/usr/share/nginx/html/swagger.yaml ports: - 3200:8080
Client IDを環境変数に設定しSwagger UIを起動します。
OAUTH_CLIENT_ID=CDKからアウトプットされたClient ID docker compose up
Swagger UIを使ったAPI呼び出しは下記のブログを参考にしてください。(今回の手順ではClient IDが予め設定されています、Client Secretは設定する必要がありません。)
Swagger 3.0のOAuth認証にCognito User PoolsのOAuth Clientを使う | DevelopersIO
あとがき
今回作成したコードはこちらで公開しています。 intercept6/cdk-demo-apigw-with-cognito
OpenAPI定義へのパラメーターは手動で書き換えていますが、CDKのアウトプットはjsonに書き出すことが可能なのでJinja2などを使えば自動的に更新することも出来そうだなーと思いました。ただ、カスタムドメインの割当ができれば大抵は不要なので必要なケースは限定的ですね。